package pl.com.qapps.unity;

import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import pl.com.qapps.unity.LauncherPb.LauncherEvent;
import pl.com.qapps.unity.LauncherPb.LauncherEvent.Property;
import pl.com.qapps.unity.LauncherPb.LauncherEvent.Value;
import pl.com.qapps.unity.UnityPb.Request;
import pl.com.qapps.unity.UnityPb.Response;

/**
 * Represents the launcher of single application in Unity launcher. <br>
 * {@link UnityLauncher} objects are ThreadSafe. <br>
 * All methods can throw {@linkplain LauncherException} if any unexpected error
 * occurs, which is runtime exception due to assumption that it happens really
 * rarely, if ever, most likely caused by implementation oversight.
 * 
 * @author Jakub Dykowski
 */
public final class UnityLauncher {
	private static Logger log = Logger.getLogger(UnityLauncher.class.getName());
	static {
		try {
			// logging
			FileHandler fh = new FileHandler("unity_launcher.log", true);
			fh.setFormatter(new SimpleFormatter());
			log.addHandler(fh);
			log.setLevel(Level.OFF);
		} catch (SecurityException | IOException e) {
			e.printStackTrace(System.err);
		}
		// register protobuf extension
		Unity.register(LauncherPb.launcherResponse);
	}

	/*
	 * Launcher's cache
	 */
	private static final Map<String, UnityLauncher> launchers = new WeakHashMap<>();

	/**
	 * The file name should be a '.desktop' file name that exists on the system.
	 * Returned object does not guarantee that the launcher really exists.
	 * 
	 * @param name
	 *            should ends with '.desktop'
	 * @return non <code>null</code>, possibly not existing
	 * @throws AppNotFoundException
	 *             if given app does not exists
	 * @throws LauncherException
	 *             if any internal error occurred
	 */
	public static UnityLauncher getForDesktopFile(String name)
			throws LauncherException {
		return getForAppUri("application://" + name);
	}

	/**
	 * Get launcher for application uri (e.g. 'application://Eclipse.desktop').
	 * 
	 * @param uri
	 * @return the UnityLauncher instance
	 * @throws LauncherException
	 *             if any unexpected error occurs
	 * @throws NullPointerException
	 *             if given uri is null
	 */
	public static UnityLauncher getForAppUri(String uri)
			throws LauncherException, NullPointerException {
		Objects.requireNonNull(uri);
		synchronized (launchers) {
			UnityLauncher cached = launchers.get(uri);
			if (cached == null) {
				launchers.put(uri, cached = new UnityLauncher(uri));
			}
			return cached;
		}
	}

	private Quicklist quicklist;
	private String uri;

	private UnityLauncher(String uri) throws LauncherException {
		this.uri = uri.startsWith("application://") ? uri : "application://"
				+ uri;
		set(Property.CREATE, Value.newBuilder().setBool(true).build());

	}

	/**
	 * Returns the application uri.
	 * 
	 * @return the application uri
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public String getAppUri() throws LauncherException {
		return uri;
	}

	/**
	 * Notice that it may be not visible. {@link #setCountVisible(boolean)}
	 * 
	 * @return the number displayed in right upper-right corner of the launcher
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public int getCount() throws LauncherException {
		return get(Property.COUNT).getInt();
	}

	/**
	 * To be displayed you may need to call {@link #setCountVisible(boolean)}
	 * first.
	 * 
	 * @param count
	 *            the number displayed in right upper-right corner of the
	 *            launcher, has to be at least 0
	 * @return the {@link UnityLauncher} itself
	 * @throws IllegalArgumentException
	 *             if count is < than 0
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setCount(int count) throws IllegalArgumentException,
			LauncherException {
		if (count < 0)
			throw new IllegalArgumentException("count has to be >= 0 but "
					+ count + " given");
		set(Property.COUNT, Value.newBuilder().setInt(count).build());
		return this;
	}

	/**
	 * Checks if the count number is visible.
	 * 
	 * @return <code>true</code> if visible, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isCountVisible() throws LauncherException {
		return get(Property.COUNT_VISIBLE).getBool();
	}

	/**
	 * Sets whether the count number should be visible or not.
	 * 
	 * @param visible
	 *            <code>true</code> if visible <code>false</code> otherwise
	 * @return the {@link UnityLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setCountVisible(boolean visible)
			throws LauncherException {
		set(Property.COUNT_VISIBLE, Value.newBuilder().setBool(visible).build());
		return this;
	}

	/**
	 * Checks the progress status which may or may not be visible on the
	 * launcher.
	 * 
	 * @return progress status, a number between 0 and 1 (both inclusive)
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public double getProgress() throws LauncherException {
		return get(Property.PROGRESS).getDouble();
	}

	/**
	 * Sets the progress status which may or may not be visible ont the
	 * launcher.
	 * 
	 * @param progress
	 *            a number between 0 and 1 (both inclusive)
	 * @return the {@link UnityLauncher} itself
	 * @throws IllegalArgumentException
	 *             if progress is less than 0 or grower than 1
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setProgress(double progress)
			throws IllegalArgumentException, LauncherException {
		if (progress < 0 || progress > 1)
			throw new IllegalArgumentException(
					"progress has to be < 1 and > 0 but " + progress + " given");
		set(Property.PROGRESS, Value.newBuilder().setDouble(progress).build());
		return this;
	}

	/**
	 * Checks whether the progress status is visible.
	 * 
	 * @return <code>true</code> if visible, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isProgressVisible() throws LauncherException {
		return get(Property.PROGRESS_VISIBLE).getBool();
	}

	/**
	 * Sets whether the progress status should be visible or not.
	 * 
	 * @param visible
	 *            <code>true</code> if visible, <code>false</code> otherwise
	 * @return the {@link UnityLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setProgressVisible(boolean visible)
			throws LauncherException {
		set(Property.PROGRESS_VISIBLE, Value.newBuilder().setBool(visible)
				.build());
		return this;
	}

	/**
	 * Checks whether launcher is set to be urgent or not. Urgency means that it
	 * will be attracting user attention.
	 * 
	 * @return <code>true</code> if is urgent, <code>false</code> otherwise
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public boolean isUrgent() throws LauncherException {
		return get(Property.URGENT).getBool();
	}

	/**
	 * Sets whether the launcher should be marked as urgent or not.
	 * 
	 * @param urgent
	 *            <code>true</code> if urgent, <code>false</code> otherwise
	 * @return the {@link UnityLauncher} itself
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setUrgent(boolean urgent) throws LauncherException {
		set(Property.URGENT, Value.newBuilder().setBool(urgent).build());
		return this;
	}

	/**
	 * Returns the previously assigned quicklist or <code>null</code> if there
	 * is none. The default value is <code>null</code>.
	 * 
	 * @return quicklist or <code>null</code> if none
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public Quicklist getQuicklist() {
		return this.quicklist;
	}

	// private Lock quicklistLock = new ReentrantLock();

	/**
	 * Sets the quicklist available after right click one the Unity launcher. If
	 * there where previous one it gets replaced.
	 * 
	 * @param quicklist
	 *            <code>null</code> are acceptable either as empty quicklists
	 * @return
	 * @throws LauncherException
	 *             if any internal exception occurred
	 */
	public UnityLauncher setQuicklist(Quicklist quicklist)
			throws LauncherException {
		if (quicklist == null)
			quicklist = new Quicklist();

		set(Property.QUICKLIST,
				Value.newBuilder().setQuicklist(quicklist.toProto()).build());
		this.quicklist = quicklist;
		return this;
	}

	/**
	 * Two instances of {@link UnityLauncher} are considered equal as long as
	 * they reference to the same Unity launcher, they don't need to be actually
	 * the same object.
	 */
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof UnityLauncher))
			return false;
		UnityLauncher other = (UnityLauncher) obj;
		return uri.equals(other.uri);
	}

	@Override
	public int hashCode() {
		return uri.hashCode();
	}

	@Override
	protected void finalize() throws Throwable {
		set(Property.CREATE, Value.newBuilder().setBool(false).build());
	}

	@Override
	public String toString() {
		return "<" + getClass().getSimpleName() + " " + uri + ">";
	}

	private void set(Property property, Value value) throws LauncherException {
		send(LauncherEvent.newBuilder().setName(property).setValue(value)
				.setApp(uri).build());
	}

	private Value get(Property property) throws LauncherException {
		Response res = send(LauncherEvent.newBuilder().setName(property)
				.setApp(uri).build());
		if (!res.hasExtension(LauncherPb.launcherResponse)) {
			throw new LauncherException("no launcher response returned", true);
		}
		return res.getExtension(LauncherPb.launcherResponse);
	}

	private Response send(LauncherEvent evt) throws LauncherException {
		try {
			return Unity.connect().send(
					Request.newBuilder()
							.setExtension(LauncherPb.launcherEvent, evt)
							.build());
		} catch (IllegalStateException | IOException e) {
			throw new LauncherException("failed to update item status", e);
		} catch (UnityException e) {
			throw new LauncherException(e);
		}
	}
}
